package com.cnap.multiclouldmgmt.util;

import com.cnap.multiclouldmgmt.model.ssh.SshConnection;
import com.cnap.multiclouldmgmt.model.ssh.SshResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.session.ClientSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 版本协商
 * SSH协议目前存在SSH1.X（SSH2.0之前的版本）
 * 和SSH2.0版本。SSH2.0协议相比SSH1.X协议来说，
 * 在结构上做了扩展，可以支持更多的认证方法和密钥交换方法，
 * 同时提高了服务能力。SSH服务器和客户端通过协商确定最终使用的SSH版本号。
 *
 * 算法协商
 * SSH支持多种加密算法，
 * 双方根据各自支持的算法，
 * 协商出最终用于产生会话密钥的密钥交换算法、
 * 用于数据信息加密的加密算法、
 * 用于进行数字签名和认证的
 * 公钥算法以及用于数据完整性保护的HMAC算法。
 *
 * 密钥交换
 * 服务器和客户端通过密钥交换算法，
 * 动态生成共享的会话密钥和会话ID，
 * 建立加密通道。
 * 会话密钥主要用于后续数据传输的加密，
 * 会话ID用于在认证过程中标识该SSH连接。
 *
 * 用户认证
 * SSH客户端向服务器端发起认证请求，
 * 服务器端对客户端进行认证。
 * SSH支持以下几种认证方式：
 * 密码（password）认证：
 *      客户端通过用户名和密码的方式进行认证，
 *      将加密后的用户名和密码发送给服务器，
 *      服务器解密后与本地保存的用户名和密码进行对比，
 *      并向客户端返回认证成功或失败的消息。
 *
 * 密钥（publickey）认证：
 *      客户端通过用户名，
 *      公钥以及公钥算法等
 *      信息来与服务器进行认证。
 *
 * password-publickey认证：
 *       指用户需要同时满足密码认证和密钥认证才能登录。
 *
 * all认证：
 *          只要满足密码认证和密钥认证其中一种即可。
 *
 * 会话请求
 * 认证通过后，SSH客户端向服务器端发送会话请求，
 * 请求服务器提供某种类型的服务，即请求与服务器建立相应的会话。
 *
 * 会话交互
 * 会话建立后，SSH服务器端和客户端在该会话上进行数据信息的交互。
 *
 * 使用PuTTY和OpenSSH
 * PuTTY是Windows上经典的免费SSH连接工具，
 * 通常用于使用SSH协议远程登录设备，
 * 最新版本可以在PuTTY官网下载。
 *
 * OpenSSH是SSH协议的开源实现，
 * 支持在Unix操作系统上运行，最新版本可以在OpenSSH官网下载。目前Windows10已经包含OpenSSH客户端和服务器软件，可在“设置—应用—应用与功能—可选功能”中搜索安装。
 *
 * SSH密钥
 * 对称加密和非对称加密
 * 提高安全性的基本方式就是加密，
 * 加密算法通过密钥将明文转换为密文进行安全传输。
 * SSH在工作过程中结合使用了对称加密和非对称加密两种类型的算法，
 * 通过事先生成的SSH密钥来保证信息传输的安全性。
 */
public final class SshUtils {

    private final static Logger LOGGER = LoggerFactory.getLogger(SshUtils.class);

    /**
     * 配置sshclient
     */
    private static SshClient sshClient = null;

    /**
     * 默认使用
     * @return
     */
    private static SshClient getClient(){
        if (sshClient == null){
            sshClient = SshClient.setUpDefaultClient();
        }

        return sshClient;
    }
    public static void setSshClient(SshClient newSshClient){
        sshClient = newSshClient;
    }

    public static SshResponse runCommand(SshConnection conn, String cmd){

        //先默认超时120分钟
        long timeout = 120 * 60L;
        return runCommand(conn,cmd,timeout,false);
    }

    public static SshResponse runCommand(SshConnection conn, String cmd,boolean showEventFlag){

        //先默认超时120分钟
        long timeout = 120 * 60L;
        return runCommand(conn,cmd,timeout,showEventFlag);
    }

    public static SshResponse runCommand(SshConnection conn, String cmd,long timeout){

        return runCommand(conn,cmd,timeout,false);
    }

    public static SshResponse runCommand(SshConnection conn, String cmd, long timeout,boolean showEventFlag){
        if (!StringUtils.equals(conn.getHostname(),CnapCloudMgmtConstants.TEST_ADMIN)){
            sshClient = null;
        }
        SshClient client = getClient();

        try {
            // Open the client
            client.start();

            // Connect to the server
            ConnectFuture cf = client.connect(conn.getUsername(), conn.getHostname(), 22);
            ClientSession session = cf.verify().getSession();
            session.addPasswordIdentity(conn.getPassword());
            session.auth().verify(TimeUnit.SECONDS.toMillis(timeout));

            // Create the exec and channel its output/error streams
            ChannelExec ce = session.createExecChannel(cmd);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ByteArrayOutputStream err = new ByteArrayOutputStream();
            ce.setOut(out);
            ce.setErr(err);

            //Execute and wait
            ce.open();
            Set<ClientChannelEvent> events =
                    ce.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(timeout));
            session.close(false);

            //Check if timed out
            if (events.contains(ClientChannelEvent.TIMEOUT)) {
                LOGGER.error("ssh connect timeout targetIp={} cmd={}",conn.getHostname(),cmd);
            }
            if (showEventFlag){
                for (ClientChannelEvent event:events) {
                    LOGGER.warn("ssh connect result cmd={} event={}", cmd, event.toString());
                }
            }

            String outString = "";
            String errString = "";
            int code = 0;
            if (out.size() > 0){
                outString = out.toString();
            }
            if(err.size() > 0){
                errString = err.toString();
            }
            if (ce.getExitStatus() != null){
                code = ce.getExitStatus();
            }

            return new SshResponse(outString, errString, code);

        }catch (Exception e){
            LOGGER.error("ssh connect fail targetIp={} cmd={}",conn.getHostname(),cmd,e);
        }
        finally {
            client.stop();
        }

        return new SshResponse("cnap ssh connect fail","cnap ssh connect fail",1);
    }

}

